package org.zebra.mybatis.plus.ext.audit;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import com.baomidou.mybatisplus.annotation.IEnum;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import lombok.Data;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.select.FromItem;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Reader;
import java.lang.reflect.Field;
import java.sql.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.Date;

/**
 * @author zhanghongbin
 */
public abstract class DataAudit {

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    public abstract OperationResult process();

    protected List<DataChangedRecord> compareAndGetUpdatedColumnDatas(
            Map<String, Set<String>> ignoredTableColumns,
            String tableName,
            OriginalDataObj originalDataObj,
            Map<String, Object> columnNameValMap) {
        final Set<String> ignoredColumns = ignoredTableColumns.get(tableName.toUpperCase());
        if (originalDataObj.isEmpty()) {
            DataChangedRecord oneRecord = new DataChangedRecord();
            List<DataColumnChangeResult> updateColumns = new ArrayList<>(columnNameValMap.size());
            for (Map.Entry<String, Object> ety : columnNameValMap.entrySet()) {
                String columnName = ety.getKey();
                if (ignoredColumns == null || !ignoredColumns.contains(columnName.toUpperCase())) {
                    updateColumns.add(DataColumnChangeResult.constrcutByUpdateVal(columnName, ety.getValue()));
                }
            }
            oneRecord.setUpdatedColumns(updateColumns);
            return Collections.singletonList(oneRecord);
        }
        List<DataChangedRecord> originalDataList = originalDataObj.getOriginalDataObj();
        List<DataChangedRecord> updateDataList = new ArrayList<>(originalDataList.size());
        for (DataChangedRecord originalData : originalDataList) {
            if (originalData.hasUpdate(columnNameValMap, ignoredColumns)) {
                updateDataList.add(originalData);
            }
        }
        return updateDataList;
    }

    protected String buildOriginalData(
            int batchUpdateLimit,
            boolean batchUpdateLimitationOpened,
            Map<String, Integer> batchUpdateLimitMap,
            Select selectStmt,
            MappedStatement mappedStatement,
            BoundSql boundSql,
            Connection connection,
            String logicDeleteColumnName) {
        try (PreparedStatement statement = connection.prepareStatement(selectStmt.toString())) {
            DefaultParameterHandler parameterHandler =
                    new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), boundSql);
            parameterHandler.setParameters(statement);
            ResultSet resultSet = statement.executeQuery();
            final ResultSetMetaData metaData = resultSet.getMetaData();
            int columnCount = metaData.getColumnCount();
            JSONArray jsonArray = new JSONArray();
            int count = 0;
            while (resultSet.next()) {
                ++count;
                if (checkTableBatchLimitExceeded(
                        batchUpdateLimit, batchUpdateLimitationOpened, batchUpdateLimitMap, selectStmt, count)) {
                    logger.error("batch delete limit exceed: count={}, BATCH_UPDATE_LIMIT={}", count, batchUpdateLimit);
                    throw DataUpdateLimitationException.DEFAULT;
                }
                JSONObject jsonObject = new JSONObject();
                for (int i = 1; i <= columnCount; ++i) {
                    String key = metaData.getColumnName(i);
                    if (key.equalsIgnoreCase(logicDeleteColumnName)) {
                        continue;
                    }
                    Object value;
                    Object res = resultSet.getObject(i);
                    if (res instanceof Clob) {
                        value = DataColumnChangeResult.convertClob((Clob) res);
                    } else {
                        value = res;
                    }
                    jsonObject.putOnce(key, value);
                }
                jsonArray.add(jsonObject);
            }
            resultSet.close();
            return jsonArray.toString();
        } catch (Exception e) {
            if (e instanceof DataUpdateLimitationException) {
                throw (DataUpdateLimitationException) e;
            }
            logger.error("try to get record tobe deleted for selectStmt={}", selectStmt, e);
            return "failed to get original data";
        }
    }

    /**
     * 防止出现全表批量更新
     * 默认一次更新不超过1000条
     *
     * @param batchUpdateLimit            条数
     * @param batchUpdateLimitationOpened 是否开启
     * @param batchUpdateLimitMap         数据
     * @param selectStmt                  查询条件对象
     * @param count                       数量
     * @return true, false
     */
    protected boolean checkTableBatchLimitExceeded(
            int batchUpdateLimit,
            boolean batchUpdateLimitationOpened,
            Map<String, Integer> batchUpdateLimitMap,
            Select selectStmt,
            int count) {
        if (!batchUpdateLimitationOpened) {
            return false;
        }
        final PlainSelect selectBody = (PlainSelect) selectStmt.getSelectBody();
        final FromItem fromItem = selectBody.getFromItem();
        if (fromItem instanceof Table) {
            Table fromTable = (Table) fromItem;
            final String tableName = fromTable.getName().toUpperCase();
            if (!batchUpdateLimitMap.containsKey(tableName)) {
                if (count > batchUpdateLimit) {
                    logger.error(
                            "batch update limit exceed for tableName={}, BATCH_UPDATE_LIMIT={}, count={}",
                            tableName,
                            batchUpdateLimit,
                            count);
                    return true;
                }
                return false;
            }
            final Integer limit = batchUpdateLimitMap.get(tableName);
            if (count > limit) {
                logger.error(
                        "batch update limit exceed for configured tableName={}, BATCH_UPDATE_LIMIT={}, count={}",
                        tableName,
                        limit,
                        count);
                return true;
            }
            return false;
        }
        return count > batchUpdateLimit;
    }

    protected Map<String, Object> getColumn(
            TableInfo tableInfo,
            BoundSql updateSql,
            Map<String, Object> columnNameValMap,
            Map<Integer, String> columnSetIndexMap,
            List<Column> selectItemsFromUpdateSql) {
        Map<String, String> relatedColumnsUpperCaseWithoutUnderline = new HashMap<>(selectItemsFromUpdateSql.size(), 1);
        for (Column item : selectItemsFromUpdateSql) {
            // FIRSTNAME: FIRST_NAME/FIRST-NAME/FIRST$NAME/FIRST.NAME
            relatedColumnsUpperCaseWithoutUnderline.put(
                    item.getColumnName().replaceAll("[._\\-$]", "").toUpperCase(),
                    item.getColumnName().toUpperCase());
        }
        MetaObject metaObject = SystemMetaObject.forObject(updateSql.getParameterObject());
        int index = 0;
        for (ParameterMapping parameterMapping : updateSql.getParameterMappings()) {
            String propertyName = parameterMapping.getProperty();
            if (propertyName.startsWith("ew.paramNameValuePairs")) {
                ++index;
                continue;
            }
            String[] arr = propertyName.split("\\.");
            String propertyNameTrim = arr[arr.length - 1].replace("_", "").toUpperCase();
            try {
                final String columnName =
                        columnSetIndexMap.getOrDefault(index++, getColumnNameByProperty(propertyNameTrim, tableInfo));
                if (relatedColumnsUpperCaseWithoutUnderline.containsKey(propertyNameTrim)) {
                    final String colkey = relatedColumnsUpperCaseWithoutUnderline.get(propertyNameTrim);
                    Object valObj = metaObject.getValue(propertyName);
                    if (valObj instanceof IEnum) {
                        valObj = ((IEnum<?>) valObj).getValue();
                    } else if (valObj instanceof Enum) {
                        valObj = getEnumValue((Enum) valObj);
                    }
                    if (columnNameValMap.containsKey(colkey)) {
                        columnNameValMap.put(
                                relatedColumnsUpperCaseWithoutUnderline.get(propertyNameTrim),
                                String.valueOf(columnNameValMap.get(colkey))
                                        .replace("?", valObj == null ? "" : valObj.toString()));
                    }
                    if (columnName != null && !columnNameValMap.containsKey(columnName)) {
                        columnNameValMap.put(columnName, valObj);
                    }
                } else if (columnName != null) {
                    columnNameValMap.put(columnName, metaObject.getValue(propertyName));
                }
            } catch (Exception e) {
                logger.warn("get value error,propertyName:{},parameterMapping:{}", propertyName, parameterMapping);
            }
        }
        dealWithUpdateWrapper(columnSetIndexMap, columnNameValMap, updateSql);
        return columnNameValMap;
    }

    private Object getEnumValue(Enum enumVal) {
        Optional<String> enumValueFieldName = MybatisEnumTypeHandler.findEnumValueFieldName(enumVal.getClass());
        if (enumValueFieldName.isPresent()) {
            return SystemMetaObject.forObject(enumVal).getValue(enumValueFieldName.get());
        }
        return enumVal;
    }

    private String getColumnNameByProperty(String propertyName, TableInfo tableInfo) {
        // for (TableInfo tableInfo : TableInfoHelper.getTableInfos()) {
        //   if (tableName.equalsIgnoreCase(tableInfo.getTableName())) {
        // 主键处理
        if (tableInfo == null) {
            return propertyName;
        }
        if (propertyName.equalsIgnoreCase(tableInfo.getKeyProperty())) {
            return tableInfo.getKeyColumn().toUpperCase();
        }
        final List<TableFieldInfo> fieldList = tableInfo.getFieldList();
        if (CollectionUtils.isEmpty(fieldList)) {
            return propertyName;
        }
        for (TableFieldInfo tableFieldInfo : fieldList) {
            if (propertyName.equalsIgnoreCase(tableFieldInfo.getProperty())) {
                return tableFieldInfo.getColumn().toUpperCase();
            }
        }
        return propertyName;
        //   }
        //  }
        // return propertyName;
    }

    @SuppressWarnings("rawtypes")
    private void dealWithUpdateWrapper(
            Map<Integer, String> columnSetIndexMap, Map<String, Object> columnNameValMap, BoundSql updateSql) {
        if (columnSetIndexMap.size() <= columnNameValMap.size()) {
            return;
        }
        MetaObject mpgenVal = SystemMetaObject.forObject(updateSql.getParameterObject());
        Object ew = mpgenVal.getValue("ew");
        if (ew instanceof UpdateWrapper || ew instanceof LambdaUpdateWrapper) {
            final String sqlSet = ew instanceof UpdateWrapper
                    ? ((UpdateWrapper) ew).getSqlSet()
                    : ((LambdaUpdateWrapper) ew).getSqlSet(); // columnName=#{val}
            if (sqlSet == null) {
                return;
            }
            MetaObject ewMeta = SystemMetaObject.forObject(ew);
            final Map paramNameValuePairs = (Map) ewMeta.getValue("paramNameValuePairs");
            String[] setItems = sqlSet.split(",");
            for (String setItem : setItems) {
                // age=#{ew.paramNameValuePairs.MPGENVAL1}
                String[] nameAndValuePair = setItem.split("=", 2);
                if (nameAndValuePair.length == 2) {
                    String setColName = nameAndValuePair[0].trim().toUpperCase();
                    String setColVal = nameAndValuePair[1].trim(); // #{.mp}
                    if (columnSetIndexMap.containsValue(setColName)) {
                        String[] mpGenKeyArray = setColVal.split("\\.");
                        String mpGenKey = mpGenKeyArray[mpGenKeyArray.length - 1].replace("}", "");
                        final Object setVal = paramNameValuePairs.get(mpGenKey);
                        if (setVal instanceof IEnum) {
                            columnNameValMap.put(setColName, String.valueOf(((IEnum<?>) setVal).getValue()));
                        } else {
                            columnNameValMap.put(setColName, String.valueOf(setVal));
                        }
                    }
                }
            }
        }
    }

    /**
     * 获取逻辑删除字段名
     *
     * @param entityClass 表名
     * @return 名称
     */
    protected String getLogicDeleteFieldName(Class<?> entityClass) {
        Field[] fields = cn.hutool.core.util.ReflectUtil.getFields(entityClass);
        for (Field field : fields) {
            TableLogic tableLogic = field.getAnnotation(TableLogic.class);
            if (tableLogic != null) {
                return field.getName();
            }
        }
        return "";
    }

    @Data
    public static class DataColumnChangeResult {

        private String columnName;
        private Object originalValue;
        private Object updateValue;

        @SuppressWarnings("rawtypes")
        public boolean isDataChanged() {
            // if (!Objects.equals(originalValue, updateValue)) {
            if (originalValue instanceof Clob) {
                String originalStr = convertClob((Clob) originalValue);
                setOriginalValue(originalStr);
                // return !originalStr.equals(updateValue);
            }
            //                if (originalValue instanceof Comparable) {
            //                    Comparable original = (Comparable) originalValue;
            //                    Comparable update = (Comparable) updateValue;
            //                    try {
            //                        return update == null || original.compareTo(update) != 0;
            //                    } catch (Exception e) {
            //                        return true;
            //                    }
            //                }
            return true;
            // }
            // return false;
        }

        public static String convertClob(Clob clobObj) {
            try {
                return clobObj.getSubString(0, (int) clobObj.length());
            } catch (Exception e) {
                try (Reader is = clobObj.getCharacterStream()) {
                    char[] chars = new char[64];
                    int readChars;
                    StringBuilder sb = new StringBuilder();
                    while ((readChars = is.read(chars)) != -1) {
                        sb.append(chars, 0, readChars);
                    }
                    return sb.toString();
                } catch (Exception e2) {
                    // ignored
                    return "unknown clobObj";
                }
            }
        }

        public static DataColumnChangeResult constrcutByUpdateVal(String columnName, Object updateValue) {
            DataColumnChangeResult res = new DataColumnChangeResult();
            res.setColumnName(columnName);
            res.setUpdateValue(updateValue);
            return res;
        }

        public static DataColumnChangeResult constrcutByOriginalVal(String columnName, Object originalValue) {
            DataColumnChangeResult res = new DataColumnChangeResult();
            res.setColumnName(columnName);
            res.setOriginalValue(originalValue);
            return res;
        }

        public String generateDataStr() {
            if (ObjUtil.isEmpty(originalValue) && ObjUtil.isEmpty(updateValue)) {
                return "";
            }
            String oValue = convertDoubleQuotes(originalValue);
            String upValue = convertDoubleQuotes(updateValue);
            if (oValue.equals(upValue)) return "";
            StringBuilder sb = new StringBuilder();
            sb.append("\"")
                    .append(columnName.toLowerCase())
                    .append("\"")
                    .append(":")
                    .append("\"")
                    .append(oValue)
                    .append("->")
                    .append(upValue)
                    .append("\"")
                    .append(",");
            return sb.toString();
        }

        public String convertDoubleQuotes(Object obj) {
            if (obj == null) {
                return "null";
            }
            if (obj instanceof LocalDateTime) {
                LocalDateTime localDateTime = (LocalDateTime) obj;
                return localDateTime.format(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN));
            }
            if (obj instanceof Date) {
                Date date = (Date) obj;
                return DateUtil.format(date, DatePattern.NORM_DATETIME_PATTERN);
            }
            return obj.toString().replace("\"", "\\\"");
        }
    }

    @Data
    public static class DataChangedRecord {

        private String pkColumnName;
        private Object pkColumnVal;
        private List<DataColumnChangeResult> originalColumnDatas;
        private List<DataColumnChangeResult> updatedColumns;

        public boolean hasUpdate(Map<String, Object> columnNameValMap, Set<String> ignoredColumns) {
            if (originalColumnDatas == null) {
                return true;
            }
            boolean hasUpdate = false;
            updatedColumns = new ArrayList<>(originalColumnDatas.size());
            for (DataColumnChangeResult originalColumn : originalColumnDatas) {
                final String columnName = originalColumn.getColumnName().toUpperCase();
                if (ignoredColumns != null
                        && ignoredColumns.contains(
                                originalColumn.getColumnName().toUpperCase())) {
                    continue;
                }
                Object updatedValue = columnNameValMap.get(columnName);
                hasUpdate = originalColumn.isDataChanged();
                // if (originalColumn.isDataChanged(updatedValue)) {
                originalColumn.setUpdateValue(updatedValue);
                updatedColumns.add(originalColumn);
                //  }
            }
            return hasUpdate;
        }

        public String generateUpdatedDataStr() {
            StringBuilder sb = new StringBuilder();
            sb.append("{");
            if (pkColumnName != null) {
                sb.append("\"")
                        .append(pkColumnName.toLowerCase())
                        .append("\"")
                        .append(":")
                        .append("\"")
                        .append(convertDoubleQuotes(pkColumnVal))
                        .append("\"")
                        .append(",");
            }
            for (DataColumnChangeResult update : updatedColumns) {
                sb.append(update.generateDataStr());
            }
            sb.replace(sb.length() - 1, sb.length(), "}");
            return sb.toString();
        }

        public String convertDoubleQuotes(Object obj) {
            if (obj == null) {
                return null;
            }
            return obj.toString().replace("\"", "\\\"");
        }
    }

    @Data
    public static class OriginalDataObj {

        private List<DataChangedRecord> originalDataObj;

        public boolean isEmpty() {
            return originalDataObj == null || originalDataObj.isEmpty();
        }
    }
}
